{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "09f7126f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c6ab2b72",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "import seaborn as sns\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a08d6044",
   "metadata": {},
   "source": [
    "## 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df055add",
   "metadata": {},
   "source": [
    "## 通用导入函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "74bcbf7b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "993c86ce",
   "metadata": {},
   "source": [
    "## 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "722d1e40",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>APSDTRDAT</th>\n",
       "      <th>APSDTRTIME</th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>APSDTRCOD</th>\n",
       "      <th>APSDTRAMT</th>\n",
       "      <th>APSDABS</th>\n",
       "      <th>APSDTRCHL</th>\n",
       "      <th>APSDFLAG</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>20131201</td>\n",
       "      <td>161612</td>\n",
       "      <td>64d3773c1ef7432af719ad377647c527</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-3.03</td>\n",
       "      <td>b9edc9a85873ce29c6f938fbe7f2f695</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>20131204</td>\n",
       "      <td>111641</td>\n",
       "      <td>6e9c9993dee7963da55000bdbfbbdace</td>\n",
       "      <td>b8f853d9f1670dd8b94814dab3db8758</td>\n",
       "      <td>0.47</td>\n",
       "      <td>5adaf0bdf909be8bece42bc3dcb32ede</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>20131202</td>\n",
       "      <td>121547</td>\n",
       "      <td>e31c2f8c467c36f2919c3e2dfc456792</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-3.69</td>\n",
       "      <td>d822ff958d2e34fd0f3d9eacfaf6b460</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>20131204</td>\n",
       "      <td>200337</td>\n",
       "      <td>8997f1671ecc5e8e31cc3995fdeeb2fc</td>\n",
       "      <td>6c6f58186b3a7737977acbe5f8068b58</td>\n",
       "      <td>3.69</td>\n",
       "      <td>00537e025c6716eca8f53090d67be7d1</td>\n",
       "      <td>528963653618fdbceaf072cca4231917</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>20131202</td>\n",
       "      <td>190833</td>\n",
       "      <td>0e10769bcb9a8c61ffc905d7766403d3</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-2.72</td>\n",
       "      <td>225f339f5c6e96f9166d8721e85538b9</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   APSDTRDAT  APSDTRTIME                           CUST_NO  \\\n",
       "0   20131201      161612  64d3773c1ef7432af719ad377647c527   \n",
       "1   20131204      111641  6e9c9993dee7963da55000bdbfbbdace   \n",
       "2   20131202      121547  e31c2f8c467c36f2919c3e2dfc456792   \n",
       "3   20131204      200337  8997f1671ecc5e8e31cc3995fdeeb2fc   \n",
       "4   20131202      190833  0e10769bcb9a8c61ffc905d7766403d3   \n",
       "\n",
       "                          APSDTRCOD  APSDTRAMT  \\\n",
       "0  31b49b530284874b8d0318c5baf37201      -3.03   \n",
       "1  b8f853d9f1670dd8b94814dab3db8758       0.47   \n",
       "2  31b49b530284874b8d0318c5baf37201      -3.69   \n",
       "3  6c6f58186b3a7737977acbe5f8068b58       3.69   \n",
       "4  31b49b530284874b8d0318c5baf37201      -2.72   \n",
       "\n",
       "                            APSDABS                         APSDTRCHL  \\\n",
       "0  b9edc9a85873ce29c6f938fbe7f2f695  30b6329d64f560ec60434d0fba757ee0   \n",
       "1  5adaf0bdf909be8bece42bc3dcb32ede  30b6329d64f560ec60434d0fba757ee0   \n",
       "2  d822ff958d2e34fd0f3d9eacfaf6b460  30b6329d64f560ec60434d0fba757ee0   \n",
       "3  00537e025c6716eca8f53090d67be7d1  528963653618fdbceaf072cca4231917   \n",
       "4  225f339f5c6e96f9166d8721e85538b9  30b6329d64f560ec60434d0fba757ee0   \n",
       "\n",
       "   APSDFLAG  \n",
       "0         1  \n",
       "1         0  \n",
       "2         1  \n",
       "3         0  \n",
       "4         1  "
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_tr_aps_dtl_data = pd.read_csv('./data/Train/TRAIN_TR_APS_DTL.csv')\n",
    "train_tr_aps_dtl_data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e70968",
   "metadata": {},
   "source": [
    "## 测试集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c3ef0f15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 A_ASSET_data 已加载为 DataFrame\n",
      "数据集 A_CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_CUST_INFO_data 已加载为 DataFrame\n",
      "数据集 A_MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_TRNFLW_DTL_data 已加载为 DataFrame\n",
      "数据集 A_PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 A_TEST_NATURE_data 已加载为 DataFrame\n",
      "数据集 A_TR_APS_DTL_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "#A_tr_aps_dtl_data = pd.read_csv('./data/A/A_TR_APS_DTL.csv')\n",
    "#A_tr_aps_dtl_data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6a9129f",
   "metadata": {},
   "source": [
    "# 特征工程"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12051a37",
   "metadata": {},
   "source": [
    "## 数据探查"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "390b0ab7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "活期交易明细表(TR_APS_DTL)数据探查\n",
      "================================================================================\n",
      "\n",
      "训练集基本信息:\n",
      "数据形状: (5361850, 8)\n",
      "客户数: 67,214\n",
      "总交易笔数: 5,361,850\n",
      "\n",
      "\n",
      "列信息:\n",
      "APSDTRDAT       int64\n",
      "APSDTRTIME      int64\n",
      "CUST_NO        object\n",
      "APSDTRCOD      object\n",
      "APSDTRAMT     float64\n",
      "APSDABS        object\n",
      "APSDTRCHL      object\n",
      "APSDFLAG        int64\n",
      "dtype: object\n",
      "\n",
      "\n",
      "缺失值统计:\n",
      "客户数: 67,214\n",
      "总交易笔数: 5,361,850\n",
      "\n",
      "\n",
      "列信息:\n",
      "APSDTRDAT       int64\n",
      "APSDTRTIME      int64\n",
      "CUST_NO        object\n",
      "APSDTRCOD      object\n",
      "APSDTRAMT     float64\n",
      "APSDABS        object\n",
      "APSDTRCHL      object\n",
      "APSDFLAG        int64\n",
      "dtype: object\n",
      "\n",
      "\n",
      "缺失值统计:\n",
      "APSDTRDAT     0\n",
      "APSDTRTIME    0\n",
      "CUST_NO       0\n",
      "APSDTRCOD     0\n",
      "APSDTRAMT     0\n",
      "APSDABS       8\n",
      "APSDTRCHL     0\n",
      "APSDFLAG      0\n",
      "dtype: int64\n",
      "\n",
      "\n",
      "前5条数据:\n",
      "APSDTRDAT     0\n",
      "APSDTRTIME    0\n",
      "CUST_NO       0\n",
      "APSDTRCOD     0\n",
      "APSDTRAMT     0\n",
      "APSDABS       8\n",
      "APSDTRCHL     0\n",
      "APSDFLAG      0\n",
      "dtype: int64\n",
      "\n",
      "\n",
      "前5条数据:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>APSDTRDAT</th>\n",
       "      <th>APSDTRTIME</th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>APSDTRCOD</th>\n",
       "      <th>APSDTRAMT</th>\n",
       "      <th>APSDABS</th>\n",
       "      <th>APSDTRCHL</th>\n",
       "      <th>APSDFLAG</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>20131201</td>\n",
       "      <td>161612</td>\n",
       "      <td>64d3773c1ef7432af719ad377647c527</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-3.03</td>\n",
       "      <td>b9edc9a85873ce29c6f938fbe7f2f695</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>20131204</td>\n",
       "      <td>111641</td>\n",
       "      <td>6e9c9993dee7963da55000bdbfbbdace</td>\n",
       "      <td>b8f853d9f1670dd8b94814dab3db8758</td>\n",
       "      <td>0.47</td>\n",
       "      <td>5adaf0bdf909be8bece42bc3dcb32ede</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>20131202</td>\n",
       "      <td>121547</td>\n",
       "      <td>e31c2f8c467c36f2919c3e2dfc456792</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-3.69</td>\n",
       "      <td>d822ff958d2e34fd0f3d9eacfaf6b460</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>20131204</td>\n",
       "      <td>200337</td>\n",
       "      <td>8997f1671ecc5e8e31cc3995fdeeb2fc</td>\n",
       "      <td>6c6f58186b3a7737977acbe5f8068b58</td>\n",
       "      <td>3.69</td>\n",
       "      <td>00537e025c6716eca8f53090d67be7d1</td>\n",
       "      <td>528963653618fdbceaf072cca4231917</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>20131202</td>\n",
       "      <td>190833</td>\n",
       "      <td>0e10769bcb9a8c61ffc905d7766403d3</td>\n",
       "      <td>31b49b530284874b8d0318c5baf37201</td>\n",
       "      <td>-2.72</td>\n",
       "      <td>225f339f5c6e96f9166d8721e85538b9</td>\n",
       "      <td>30b6329d64f560ec60434d0fba757ee0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   APSDTRDAT  APSDTRTIME                           CUST_NO  \\\n",
       "0   20131201      161612  64d3773c1ef7432af719ad377647c527   \n",
       "1   20131204      111641  6e9c9993dee7963da55000bdbfbbdace   \n",
       "2   20131202      121547  e31c2f8c467c36f2919c3e2dfc456792   \n",
       "3   20131204      200337  8997f1671ecc5e8e31cc3995fdeeb2fc   \n",
       "4   20131202      190833  0e10769bcb9a8c61ffc905d7766403d3   \n",
       "\n",
       "                          APSDTRCOD  APSDTRAMT  \\\n",
       "0  31b49b530284874b8d0318c5baf37201      -3.03   \n",
       "1  b8f853d9f1670dd8b94814dab3db8758       0.47   \n",
       "2  31b49b530284874b8d0318c5baf37201      -3.69   \n",
       "3  6c6f58186b3a7737977acbe5f8068b58       3.69   \n",
       "4  31b49b530284874b8d0318c5baf37201      -2.72   \n",
       "\n",
       "                            APSDABS                         APSDTRCHL  \\\n",
       "0  b9edc9a85873ce29c6f938fbe7f2f695  30b6329d64f560ec60434d0fba757ee0   \n",
       "1  5adaf0bdf909be8bece42bc3dcb32ede  30b6329d64f560ec60434d0fba757ee0   \n",
       "2  d822ff958d2e34fd0f3d9eacfaf6b460  30b6329d64f560ec60434d0fba757ee0   \n",
       "3  00537e025c6716eca8f53090d67be7d1  528963653618fdbceaf072cca4231917   \n",
       "4  225f339f5c6e96f9166d8721e85538b9  30b6329d64f560ec60434d0fba757ee0   \n",
       "\n",
       "   APSDFLAG  \n",
       "0         1  \n",
       "1         0  \n",
       "2         1  \n",
       "3         0  \n",
       "4         1  "
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(\"=\" * 80)\n",
    "print(\"活期交易明细表(TR_APS_DTL)数据探查\")\n",
    "print(\"=\" * 80)\n",
    "\n",
    "print(\"\\n训练集基本信息:\")\n",
    "print(f\"数据形状: {train_tr_aps_dtl_data.shape}\")\n",
    "print(f\"客户数: {train_tr_aps_dtl_data['CUST_NO'].nunique():,}\")\n",
    "print(f\"总交易笔数: {len(train_tr_aps_dtl_data):,}\")\n",
    "\n",
    "print(\"\\n\\n列信息:\")\n",
    "print(train_tr_aps_dtl_data.dtypes)\n",
    "\n",
    "print(\"\\n\\n缺失值统计:\")\n",
    "print(train_tr_aps_dtl_data.isnull().sum())\n",
    "\n",
    "print(\"\\n\\n前5条数据:\")\n",
    "train_tr_aps_dtl_data.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "0800fa05",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "数值型字段统计:\n",
      "          APSDTRDAT    APSDTRTIME     APSDTRAMT      APSDFLAG\n",
      "count  5.361850e+06  5.361850e+06  5.361850e+06  5.361850e+06\n",
      "mean   2.013112e+07  1.366316e+05 -2.190333e+00  6.469748e-01\n",
      "std    8.291785e+01  5.267685e+04  1.444642e+01  4.779105e-01\n",
      "min    2.013100e+07  0.000000e+00 -5.012900e+02  0.000000e+00\n",
      "25%    2.013102e+07  1.002540e+05 -6.050000e+00  0.000000e+00\n",
      "50%    2.013112e+07  1.355050e+05 -2.910000e+00  1.000000e+00\n",
      "75%    2.013121e+07  1.801240e+05 -1.080000e+00  1.000000e+00\n",
      "max    2.013123e+07  2.359590e+05  5.012900e+02  1.000000e+00\n",
      "\n",
      "\n",
      "分类型字段唯一值统计:\n",
      "          APSDTRDAT    APSDTRTIME     APSDTRAMT      APSDFLAG\n",
      "count  5.361850e+06  5.361850e+06  5.361850e+06  5.361850e+06\n",
      "mean   2.013112e+07  1.366316e+05 -2.190333e+00  6.469748e-01\n",
      "std    8.291785e+01  5.267685e+04  1.444642e+01  4.779105e-01\n",
      "min    2.013100e+07  0.000000e+00 -5.012900e+02  0.000000e+00\n",
      "25%    2.013102e+07  1.002540e+05 -6.050000e+00  0.000000e+00\n",
      "50%    2.013112e+07  1.355050e+05 -2.910000e+00  1.000000e+00\n",
      "75%    2.013121e+07  1.801240e+05 -1.080000e+00  1.000000e+00\n",
      "max    2.013123e+07  2.359590e+05  5.012900e+02  1.000000e+00\n",
      "\n",
      "\n",
      "分类型字段唯一值统计:\n",
      "CUST_NO: 67214 个唯一值\n",
      "CUST_NO: 67214 个唯一值\n",
      "APSDTRCOD: 297 个唯一值\n",
      "APSDABS: 12658 个唯一值\n",
      "APSDTRCOD: 297 个唯一值\n",
      "APSDABS: 12658 个唯一值\n",
      "APSDTRCHL: 46 个唯一值\n",
      "\n",
      "\n",
      "日期字段检查:\n",
      "APSDTRDAT类型: int64\n",
      "APSDTRDAT示例: [20131201, 20131204, 20131202, 20131204, 20131202, 20131203, 20131201, 20131203, 20131204, 20131203]\n",
      "\n",
      "\n",
      "时间字段检查:\n",
      "APSDTRTIME类型: int64\n",
      "APSDTRTIME示例: [161612, 111641, 121547, 200337, 190833, 204959, 144317, 55807, 53122, 193547]\n",
      "APSDTRCHL: 46 个唯一值\n",
      "\n",
      "\n",
      "日期字段检查:\n",
      "APSDTRDAT类型: int64\n",
      "APSDTRDAT示例: [20131201, 20131204, 20131202, 20131204, 20131202, 20131203, 20131201, 20131203, 20131204, 20131203]\n",
      "\n",
      "\n",
      "时间字段检查:\n",
      "APSDTRTIME类型: int64\n",
      "APSDTRTIME示例: [161612, 111641, 121547, 200337, 190833, 204959, 144317, 55807, 53122, 193547]\n"
     ]
    }
   ],
   "source": [
    "print(\"\\n数值型字段统计:\")\n",
    "numeric_cols = train_tr_aps_dtl_data.select_dtypes(include=[np.number]).columns\n",
    "print(train_tr_aps_dtl_data[numeric_cols].describe())\n",
    "\n",
    "print(\"\\n\\n分类型字段唯一值统计:\")\n",
    "cat_cols = train_tr_aps_dtl_data.select_dtypes(include=['object']).columns\n",
    "for col in cat_cols:\n",
    "    print(f\"{col}: {train_tr_aps_dtl_data[col].nunique()} 个唯一值\")\n",
    "\n",
    "print(\"\\n\\n日期字段检查:\")\n",
    "if 'APSDTRDAT' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"APSDTRDAT类型: {train_tr_aps_dtl_data['APSDTRDAT'].dtype}\")\n",
    "    print(f\"APSDTRDAT示例: {train_tr_aps_dtl_data['APSDTRDAT'].head(10).tolist()}\")\n",
    "\n",
    "print(\"\\n\\n时间字段检查:\")\n",
    "if 'APSDTRTIME' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"APSDTRTIME类型: {train_tr_aps_dtl_data['APSDTRTIME'].dtype}\")\n",
    "    print(f\"APSDTRTIME示例: {train_tr_aps_dtl_data['APSDTRTIME'].head(10).tolist()}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "a18c1580",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "交易金额分布:\n",
      "金额统计: \n",
      "count    5.361850e+06\n",
      "mean    -2.190333e+00\n",
      "std      1.444642e+01\n",
      "min     -5.012900e+02\n",
      "25%     -6.050000e+00\n",
      "50%     -2.910000e+00\n",
      "75%     -1.080000e+00\n",
      "max      5.012900e+02\n",
      "Name: APSDTRAMT, dtype: float64\n",
      "\n",
      "正交易(流入)笔数: 1,196,250\n",
      "负交易(流出)笔数: 4,150,959\n",
      "零交易笔数: 14,641\n",
      "\n",
      "\n",
      "交易码分布:\n",
      "交易码种类数: 297\n",
      "\n",
      "Top 10 交易码:\n",
      "APSDTRCOD\n",
      "31b49b530284874b8d0318c5baf37201    3291587\n",
      "33bd74e6f564742b64241dd83b78a3a2     369517\n",
      "4a236398b0b32b817f280236848959e4     229373\n",
      "6c6f58186b3a7737977acbe5f8068b58     223404\n",
      "c7927361bedc574f972e7666f957352a     177562\n",
      "9a62b9b2c5c7974d4a231c6dfc30920b     177334\n",
      "6d42bfd98c86a510c80dd6fb8ae55b86     111716\n",
      "ddf815220db9dbbf9bfacee18ad679e1     107135\n",
      "d5eb150f5c72b4458d7737247c34dd88      82936\n",
      "394f0811bd93fcda2db92f0652c64511      56395\n",
      "Name: count, dtype: int64\n",
      "\n",
      "\n",
      "交易渠道分布:\n",
      "交易码种类数: 297\n",
      "\n",
      "Top 10 交易码:\n",
      "APSDTRCOD\n",
      "31b49b530284874b8d0318c5baf37201    3291587\n",
      "33bd74e6f564742b64241dd83b78a3a2     369517\n",
      "4a236398b0b32b817f280236848959e4     229373\n",
      "6c6f58186b3a7737977acbe5f8068b58     223404\n",
      "c7927361bedc574f972e7666f957352a     177562\n",
      "9a62b9b2c5c7974d4a231c6dfc30920b     177334\n",
      "6d42bfd98c86a510c80dd6fb8ae55b86     111716\n",
      "ddf815220db9dbbf9bfacee18ad679e1     107135\n",
      "d5eb150f5c72b4458d7737247c34dd88      82936\n",
      "394f0811bd93fcda2db92f0652c64511      56395\n",
      "Name: count, dtype: int64\n",
      "\n",
      "\n",
      "交易渠道分布:\n",
      "渠道种类数: 46\n",
      "\n",
      "渠道分布:\n",
      "APSDTRCHL\n",
      "30b6329d64f560ec60434d0fba757ee0    3775031\n",
      "5f3bf070208f8beb6f3105098fbe0348     358327\n",
      "528963653618fdbceaf072cca4231917     332732\n",
      "84d18a7e959b598b08db2fd3bd7d28f0     309434\n",
      "d19277ce4d534c6a2c1759884e5472bd     239553\n",
      "c919baba7ac93bd2353e12e961ceb31b      76123\n",
      "86aae59395d138883994ef461876856e      47021\n",
      "9764fe7a437e34df0a39963593925544      26113\n",
      "ce733f44e90fa67e19c9b5dc987983fa      25261\n",
      "f35a0fe28f7d317d98ee45b867de4783      24741\n",
      "22546cb3c5eee3cfa55723599fb41383      24466\n",
      "6cd053ee85b988cf0cc00e4885014689      22261\n",
      "1e750ef5f06423c2cd2d6f55e9ba10ce      20948\n",
      "e3782fce93ffe06c64caf45de55d0ba9      18226\n",
      "203cbbbe06627928cd3e9e85b7debf25      15113\n",
      "cbf4425593cae31cede2727f47f1ca5f       9975\n",
      "28720c46427edba58bd738bcdbc0d5f7       7123\n",
      "7bb60c9bfe21c18902e83c75afaea7dd       6931\n",
      "573ab1d291897bb70a4fa7d46510cfea       5329\n",
      "4a97cc4c7feea5055f39235e54106759       5208\n",
      "be33214049497216794466e2704d115f       4904\n",
      "73788b17d554464b50a333f15ffc382a       1631\n",
      "c323f82d949011a2e1a876d9c1f23c5e       1511\n",
      "bd1778504c4891afacb71dcb7e5bc6b3        772\n",
      "05af8eedd72a12e8acacafc3f263f335        657\n",
      "37756e214f9929431418b334e802e729        473\n",
      "2be5eb78b9220256ec697d26f9d36d27        300\n",
      "b26a2071db34cc719e9a4e10889cca03        276\n",
      "bce205d9ae852ac1b9909b815586aaf9        239\n",
      "d8a03a85ba98ab024019c1b63365e1e9        230\n",
      "faa7f58154ad852d385825d4afc5411d        203\n",
      "9a81cd934a07b38aac827adceb4fdec5        177\n",
      "ac79f6e3b163ec5af9dbede7f6062c77        136\n",
      "2cdbfad25e00e8d5884ba9737d9b89ca        111\n",
      "f507b4586c9e2a63de757bc485ce472e         70\n",
      "4b38d17201aa7e8a4106acc0baf8736e         56\n",
      "af73bf95db301303dfce3ce03316ff68         49\n",
      "9be7e3d365130a6e5210ca58567c8653         45\n",
      "4542f7c10b0105bd426477c0ee837f83         35\n",
      "68c390f00aa1adbe27da3602a8b6be66         22\n",
      "11a635bfafe3670efd04e3596438b257         17\n",
      "6e448ac262659b878fa7bf9053c3e55f          9\n",
      "69927816721d418041a4c84da27c1963          4\n",
      "67729de7b4b112b736b1a65ada5224ff          3\n",
      "c1fea0619a36fb9ec65e9b8c1b92bf6e          3\n",
      "641561865b6ba7fab74aa3ba64760220          1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "\n",
      "三方交易标识分布:\n",
      "标识种类数: 2\n",
      "\n",
      "标识分布:\n",
      "APSDFLAG\n",
      "1    3468982\n",
      "0    1892868\n",
      "Name: count, dtype: int64\n",
      "渠道种类数: 46\n",
      "\n",
      "渠道分布:\n",
      "APSDTRCHL\n",
      "30b6329d64f560ec60434d0fba757ee0    3775031\n",
      "5f3bf070208f8beb6f3105098fbe0348     358327\n",
      "528963653618fdbceaf072cca4231917     332732\n",
      "84d18a7e959b598b08db2fd3bd7d28f0     309434\n",
      "d19277ce4d534c6a2c1759884e5472bd     239553\n",
      "c919baba7ac93bd2353e12e961ceb31b      76123\n",
      "86aae59395d138883994ef461876856e      47021\n",
      "9764fe7a437e34df0a39963593925544      26113\n",
      "ce733f44e90fa67e19c9b5dc987983fa      25261\n",
      "f35a0fe28f7d317d98ee45b867de4783      24741\n",
      "22546cb3c5eee3cfa55723599fb41383      24466\n",
      "6cd053ee85b988cf0cc00e4885014689      22261\n",
      "1e750ef5f06423c2cd2d6f55e9ba10ce      20948\n",
      "e3782fce93ffe06c64caf45de55d0ba9      18226\n",
      "203cbbbe06627928cd3e9e85b7debf25      15113\n",
      "cbf4425593cae31cede2727f47f1ca5f       9975\n",
      "28720c46427edba58bd738bcdbc0d5f7       7123\n",
      "7bb60c9bfe21c18902e83c75afaea7dd       6931\n",
      "573ab1d291897bb70a4fa7d46510cfea       5329\n",
      "4a97cc4c7feea5055f39235e54106759       5208\n",
      "be33214049497216794466e2704d115f       4904\n",
      "73788b17d554464b50a333f15ffc382a       1631\n",
      "c323f82d949011a2e1a876d9c1f23c5e       1511\n",
      "bd1778504c4891afacb71dcb7e5bc6b3        772\n",
      "05af8eedd72a12e8acacafc3f263f335        657\n",
      "37756e214f9929431418b334e802e729        473\n",
      "2be5eb78b9220256ec697d26f9d36d27        300\n",
      "b26a2071db34cc719e9a4e10889cca03        276\n",
      "bce205d9ae852ac1b9909b815586aaf9        239\n",
      "d8a03a85ba98ab024019c1b63365e1e9        230\n",
      "faa7f58154ad852d385825d4afc5411d        203\n",
      "9a81cd934a07b38aac827adceb4fdec5        177\n",
      "ac79f6e3b163ec5af9dbede7f6062c77        136\n",
      "2cdbfad25e00e8d5884ba9737d9b89ca        111\n",
      "f507b4586c9e2a63de757bc485ce472e         70\n",
      "4b38d17201aa7e8a4106acc0baf8736e         56\n",
      "af73bf95db301303dfce3ce03316ff68         49\n",
      "9be7e3d365130a6e5210ca58567c8653         45\n",
      "4542f7c10b0105bd426477c0ee837f83         35\n",
      "68c390f00aa1adbe27da3602a8b6be66         22\n",
      "11a635bfafe3670efd04e3596438b257         17\n",
      "6e448ac262659b878fa7bf9053c3e55f          9\n",
      "69927816721d418041a4c84da27c1963          4\n",
      "67729de7b4b112b736b1a65ada5224ff          3\n",
      "c1fea0619a36fb9ec65e9b8c1b92bf6e          3\n",
      "641561865b6ba7fab74aa3ba64760220          1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "\n",
      "三方交易标识分布:\n",
      "标识种类数: 2\n",
      "\n",
      "标识分布:\n",
      "APSDFLAG\n",
      "1    3468982\n",
      "0    1892868\n",
      "Name: count, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "print(\"\\n\\n交易金额分布:\")\n",
    "if 'APSDTRAMT' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"金额统计: \\n{train_tr_aps_dtl_data['APSDTRAMT'].describe()}\")\n",
    "    print(f\"\\n正交易(流入)笔数: {(train_tr_aps_dtl_data['APSDTRAMT'] > 0).sum():,}\")\n",
    "    print(f\"负交易(流出)笔数: {(train_tr_aps_dtl_data['APSDTRAMT'] < 0).sum():,}\")\n",
    "    print(f\"零交易笔数: {(train_tr_aps_dtl_data['APSDTRAMT'] == 0).sum():,}\")\n",
    "\n",
    "print(\"\\n\\n交易码分布:\")\n",
    "if 'APSDTRCOD' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"交易码种类数: {train_tr_aps_dtl_data['APSDTRCOD'].nunique()}\")\n",
    "    print(f\"\\nTop 10 交易码:\")\n",
    "    print(train_tr_aps_dtl_data['APSDTRCOD'].value_counts().head(10))\n",
    "\n",
    "print(\"\\n\\n交易渠道分布:\")\n",
    "if 'APSDTRCHL' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"渠道种类数: {train_tr_aps_dtl_data['APSDTRCHL'].nunique()}\")\n",
    "    print(f\"\\n渠道分布:\")\n",
    "    print(train_tr_aps_dtl_data['APSDTRCHL'].value_counts())\n",
    "\n",
    "print(\"\\n\\n三方交易标识分布:\")\n",
    "if 'APSDFLAG' in train_tr_aps_dtl_data.columns:\n",
    "    print(f\"标识种类数: {train_tr_aps_dtl_data['APSDFLAG'].nunique()}\")\n",
    "    print(f\"\\n标识分布:\")\n",
    "    print(train_tr_aps_dtl_data['APSDFLAG'].value_counts())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1e21f8a",
   "metadata": {},
   "source": [
    "## 数据预处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "651f6311",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据预处理函数已定义\n"
     ]
    }
   ],
   "source": [
    "def preprocess_tr_aps_data(df):\n",
    "    \"\"\"\n",
    "    活期交易明细表数据预处理\n",
    "    处理日期时间、异常值、创建基础衍生字段\n",
    "    \"\"\"\n",
    "    df = df.copy()\n",
    "    \n",
    "    print(f\"原始数据形状: {df.shape}\")\n",
    "    \n",
    "    # 1. 日期处理 - 先转int去除小数,再转日期\n",
    "    if df['APSDTRDAT'].dtype == 'float64':\n",
    "        df['APSDTRDAT'] = df['APSDTRDAT'].fillna(0).astype(int)\n",
    "    df['APSDTRDAT'] = df['APSDTRDAT'].astype(str).str.zfill(8)\n",
    "    df['APSDTRDAT'] = pd.to_datetime(df['APSDTRDAT'], format='%Y%m%d', errors='coerce')\n",
    "    \n",
    "    # 2. 时间处理 - 先转int去除小数,再处理\n",
    "    if 'APSDTRTIME' in df.columns:\n",
    "        if df['APSDTRTIME'].dtype == 'float64':\n",
    "            df['APSDTRTIME'] = df['APSDTRTIME'].fillna(0).astype(int)\n",
    "        df['APSDTRTIME'] = df['APSDTRTIME'].astype(str).str.zfill(6)\n",
    "        df['hour'] = df['APSDTRTIME'].str[:2].astype(int)\n",
    "        df['minute'] = df['APSDTRTIME'].str[2:4].astype(int)\n",
    "        df['second'] = df['APSDTRTIME'].str[4:6].astype(int)\n",
    "    \n",
    "    # 3. 删除日期异常的记录\n",
    "    invalid_date_count = df['APSDTRDAT'].isna().sum()\n",
    "    if invalid_date_count > 0:\n",
    "        print(f\"删除日期异常记录: {invalid_date_count} 条\")\n",
    "        df = df.dropna(subset=['APSDTRDAT'])\n",
    "    \n",
    "    # 4. 计算距今天数\n",
    "    end_date = df['APSDTRDAT'].max()\n",
    "    df['days_to_now'] = (end_date - df['APSDTRDAT']).dt.days\n",
    "    df['weeks_to_now'] = df['days_to_now'] // 7\n",
    "    df['months_to_now'] = df['days_to_now'] // 30\n",
    "    \n",
    "    # 5. 提取时间特征\n",
    "    df['year'] = df['APSDTRDAT'].dt.year\n",
    "    df['month'] = df['APSDTRDAT'].dt.month\n",
    "    df['day'] = df['APSDTRDAT'].dt.day\n",
    "    df['weekday'] = df['APSDTRDAT'].dt.weekday\n",
    "    df['is_weekend'] = df['weekday'].isin([5, 6]).astype(int)\n",
    "    df['week_of_year'] = df['APSDTRDAT'].dt.isocalendar().week.astype(int)\n",
    "    df['day_of_year'] = df['APSDTRDAT'].dt.dayofyear\n",
    "    df['quarter'] = df['APSDTRDAT'].dt.quarter\n",
    "    \n",
    "    # 6. 月初月中月末\n",
    "    df['is_month_start'] = (df['day'] <= 10).astype(int)\n",
    "    df['is_month_middle'] = ((df['day'] > 10) & (df['day'] <= 20)).astype(int)\n",
    "    df['is_month_end'] = (df['day'] > 20).astype(int)\n",
    "    \n",
    "    # 7. 时段特征\n",
    "    if 'hour' in df.columns:\n",
    "        df['time_period'] = pd.cut(df['hour'], \n",
    "                                    bins=[-1, 6, 12, 18, 24], \n",
    "                                    labels=['dawn', 'morning', 'afternoon', 'night'])\n",
    "        df['is_work_hour'] = df['hour'].between(9, 18).astype(int)\n",
    "        df['is_lunch_hour'] = df['hour'].between(11, 14).astype(int)\n",
    "    \n",
    "    # 8. 金额处理\n",
    "    if 'APSDTRAMT' in df.columns:\n",
    "        df['APSDTRAMT'] = pd.to_numeric(df['APSDTRAMT'], errors='coerce').fillna(0)\n",
    "        df['APSDTRAMT_abs'] = df['APSDTRAMT'].abs()\n",
    "        df['is_income'] = (df['APSDTRAMT'] > 0).astype(int)\n",
    "        df['is_zero_amt'] = (df['APSDTRAMT'] == 0).astype(int)\n",
    "        \n",
    "        # 金额对数(避免0)\n",
    "        df['APSDTRAMT_abs_log'] = np.log1p(df['APSDTRAMT_abs'])\n",
    "        \n",
    "        # 金额分段\n",
    "        df['amt_level'] = pd.cut(df['APSDTRAMT_abs'], \n",
    "                                  bins=[0, 100, 500, 1000, 5000, 10000, 50000, 100000, float('inf')],\n",
    "                                  labels=['level1', 'level2', 'level3', 'level4', 'level5', 'level6', 'level7', 'level8'])\n",
    "    \n",
    "    # 9. 交易码类型编码\n",
    "    if 'APSDTRCOD' in df.columns:\n",
    "        df['APSDTRCOD'] = df['APSDTRCOD'].fillna('unknown').astype(str)\n",
    "    \n",
    "    # 10. 摘要处理\n",
    "    if 'APSDABS' in df.columns:\n",
    "        df['APSDABS'] = df['APSDABS'].fillna('unknown').astype(str)\n",
    "    \n",
    "    # 11. 交易渠道处理\n",
    "    if 'APSDTRCHL' in df.columns:\n",
    "        df['APSDTRCHL'] = df['APSDTRCHL'].fillna('unknown').astype(str)\n",
    "    \n",
    "    # 12. 三方交易标识处理\n",
    "    if 'APSDFLAG' in df.columns:\n",
    "        df['APSDFLAG'] = df['APSDFLAG'].fillna('unknown').astype(str)\n",
    "        df['is_third_party'] = (df['APSDFLAG'] != 'unknown').astype(int)\n",
    "    \n",
    "    # 13. 按客户和时间排序\n",
    "    df = df.sort_values(['CUST_NO', 'APSDTRDAT', 'APSDTRTIME'], na_position='last').reset_index(drop=True)\n",
    "    \n",
    "    print(f\"预处理后数据形状: {df.shape}\")\n",
    "    print(f\"日期范围: {df['APSDTRDAT'].min()} 至 {df['APSDTRDAT'].max()}\")\n",
    "    print(f\"总天数: {(df['APSDTRDAT'].max() - df['APSDTRDAT'].min()).days} 天\")\n",
    "    print(f\"客户数: {df['CUST_NO'].nunique():,}\")\n",
    "    \n",
    "    return df\n",
    "\n",
    "print(\"数据预处理函数已定义\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "48f2c815",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "开始预处理训练集...\n",
      "原始数据形状: (5361850, 8)\n",
      "预处理后数据形状: (5361850, 34)\n",
      "日期范围: 2013-10-01 00:00:00 至 2013-12-31 00:00:00\n",
      "总天数: 91 天\n",
      "预处理后数据形状: (5361850, 34)\n",
      "日期范围: 2013-10-01 00:00:00 至 2013-12-31 00:00:00\n",
      "总天数: 91 天\n",
      "客户数: 67,214\n",
      "客户数: 67,214\n"
     ]
    }
   ],
   "source": [
    "print(\"\\n开始预处理训练集...\")\n",
    "train_tr_aps_processed = preprocess_tr_aps_data(train_tr_aps_dtl_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f4871c2",
   "metadata": {},
   "source": [
    "## 特征工程函数定义"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cac917a",
   "metadata": {},
   "source": [
    "### 1. 基础统计特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "860309fb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "基础统计特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def basic_statistics_features(df, windows=[7, 15, 30, 60, 90]):\n",
    "    \"\"\"\n",
    "    基础统计特征:交易金额、笔数、活跃度等\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    for window in tqdm(windows, desc='基础统计特征'):\n",
    "        sub_df = df[df['days_to_now'] < window].copy()\n",
    "        \n",
    "        if len(sub_df) == 0:\n",
    "            continue\n",
    "        \n",
    "        # 交易金额统计\n",
    "        amt_stats = sub_df.groupby('CUST_NO')['APSDTRAMT_abs'].agg([\n",
    "            'sum', 'mean', 'std', 'median', 'max', 'min',\n",
    "            ('q25', lambda x: x.quantile(0.25)),\n",
    "            ('q75', lambda x: x.quantile(0.75)),\n",
    "            ('skew', lambda x: x.skew()),\n",
    "            ('kurt', lambda x: x.kurtosis())\n",
    "        ]).reset_index()\n",
    "        amt_stats.columns = ['CUST_NO'] + [f'aps_amt_{window}d_{c}' for c in \n",
    "                            ['sum', 'mean', 'std', 'median', 'max', 'min', 'q25', 'q75', 'skew', 'kurt']]\n",
    "        features = features.merge(amt_stats, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 交易笔数\n",
    "        txn_count = sub_df.groupby('CUST_NO').size().reset_index(name=f'aps_count_{window}d')\n",
    "        features = features.merge(txn_count, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 交易活跃天数\n",
    "        active_days = sub_df.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "        active_days.columns = ['CUST_NO', f'aps_active_days_{window}d']\n",
    "        features = features.merge(active_days, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 日均交易金额和笔数\n",
    "        features[f'aps_daily_amt_avg_{window}d'] = features[f'aps_amt_{window}d_sum'] / window\n",
    "        features[f'aps_daily_count_avg_{window}d'] = features[f'aps_count_{window}d'] / window\n",
    "        \n",
    "        # 活跃度比率\n",
    "        features[f'aps_activity_rate_{window}d'] = features[f'aps_active_days_{window}d'] / window\n",
    "        \n",
    "        # 交易变异系数\n",
    "        features[f'aps_amt_cv_{window}d'] = features[f'aps_amt_{window}d_std'] / (features[f'aps_amt_{window}d_mean'] + 1e-5)\n",
    "        \n",
    "        # 交易范围\n",
    "        features[f'aps_amt_range_{window}d'] = features[f'aps_amt_{window}d_max'] - features[f'aps_amt_{window}d_min']\n",
    "        \n",
    "        # 四分位距\n",
    "        features[f'aps_amt_iqr_{window}d'] = features[f'aps_amt_{window}d_q75'] - features[f'aps_amt_{window}d_q25']\n",
    "        \n",
    "        # 平均每活跃日交易笔数\n",
    "        features[f'aps_count_per_active_day_{window}d'] = features[f'aps_count_{window}d'] / (features[f'aps_active_days_{window}d'] + 1)\n",
    "        \n",
    "        # 平均每活跃日交易金额\n",
    "        features[f'aps_amt_per_active_day_{window}d'] = features[f'aps_amt_{window}d_sum'] / (features[f'aps_active_days_{window}d'] + 1)\n",
    "        \n",
    "        # 笔均交易金额\n",
    "        features[f'aps_amt_per_txn_{window}d'] = features[f'aps_amt_{window}d_sum'] / (features[f'aps_count_{window}d'] + 1)\n",
    "    \n",
    "    # 跨窗口特征\n",
    "    if 30 in windows and 60 in windows:\n",
    "        features['aps_amt_30d_60d_ratio'] = features['aps_amt_30d_sum'] / (features['aps_amt_60d_sum'] + 1e-5)\n",
    "        features['aps_count_30d_60d_ratio'] = features['aps_count_30d'] / (features['aps_count_60d'] + 1e-5)\n",
    "    \n",
    "    if 7 in windows and 30 in windows:\n",
    "        features['aps_amt_7d_30d_ratio'] = features['aps_amt_7d_sum'] / (features['aps_amt_30d_sum'] + 1e-5)\n",
    "        features['aps_count_7d_30d_ratio'] = features['aps_count_7d'] / (features['aps_count_30d'] + 1e-5)\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"基础统计特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40452b86",
   "metadata": {},
   "source": [
    "### 2. 流入流出特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "b797b49f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "流入流出特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def inflow_outflow_features(df, windows=[7, 15, 30, 60, 90]):\n",
    "    \"\"\"\n",
    "    流入流出分离特征\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    df_in = df[df['is_income'] == 1].copy()\n",
    "    df_out = df[df['is_income'] == 0].copy()\n",
    "    \n",
    "    for window in tqdm(windows, desc='流入流出特征'):\n",
    "        in_sub = df_in[df_in['days_to_now'] < window]\n",
    "        out_sub = df_out[df_out['days_to_now'] < window]\n",
    "        \n",
    "        # 流入统计\n",
    "        if len(in_sub) > 0:\n",
    "            in_stats = in_sub.groupby('CUST_NO')['APSDTRAMT_abs'].agg([\n",
    "                'sum', 'mean', 'count', 'max', 'min', 'std', 'median', \n",
    "                ('skew', lambda x: x.skew())\n",
    "            ]).reset_index()\n",
    "            in_stats.columns = ['CUST_NO'] + [f'aps_in_{window}d_{c}' for c in \n",
    "                                ['sum', 'mean', 'count', 'max', 'min', 'std', 'median', 'skew']]\n",
    "            features = features.merge(in_stats, on='CUST_NO', how='left')\n",
    "            \n",
    "            # 流入活跃天数\n",
    "            in_active = in_sub.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "            in_active.columns = ['CUST_NO', f'aps_in_active_days_{window}d']\n",
    "            features = features.merge(in_active, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 流出统计\n",
    "        if len(out_sub) > 0:\n",
    "            out_stats = out_sub.groupby('CUST_NO')['APSDTRAMT_abs'].agg([\n",
    "                'sum', 'mean', 'count', 'max', 'min', 'std', 'median',\n",
    "                ('skew', lambda x: x.skew())\n",
    "            ]).reset_index()\n",
    "            out_stats.columns = ['CUST_NO'] + [f'aps_out_{window}d_{c}' for c in \n",
    "                                ['sum', 'mean', 'count', 'max', 'min', 'std', 'median', 'skew']]\n",
    "            features = features.merge(out_stats, on='CUST_NO', how='left')\n",
    "            \n",
    "            # 流出活跃天数\n",
    "            out_active = out_sub.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "            out_active.columns = ['CUST_NO', f'aps_out_active_days_{window}d']\n",
    "            features = features.merge(out_active, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 流入流出比率特征\n",
    "        features[f'aps_in_out_amt_ratio_{window}d'] = features[f'aps_in_{window}d_sum'] / (features[f'aps_out_{window}d_sum'] + 1e-5)\n",
    "        features[f'aps_in_out_count_ratio_{window}d'] = features[f'aps_in_{window}d_count'] / (features[f'aps_out_{window}d_count'] + 1e-5)\n",
    "        \n",
    "        # 净流入\n",
    "        features[f'aps_net_inflow_{window}d'] = features[f'aps_in_{window}d_sum'].fillna(0) - features[f'aps_out_{window}d_sum'].fillna(0)\n",
    "        features[f'aps_net_count_{window}d'] = features[f'aps_in_{window}d_count'].fillna(0) - features[f'aps_out_{window}d_count'].fillna(0)\n",
    "        \n",
    "        # 流入流出活跃度差异\n",
    "        features[f'aps_in_out_active_diff_{window}d'] = features[f'aps_in_active_days_{window}d'].fillna(0) - features[f'aps_out_active_days_{window}d'].fillna(0)\n",
    "        \n",
    "        # 流入流出频率差异\n",
    "        features[f'aps_in_out_freq_diff_{window}d'] = (features[f'aps_in_{window}d_count'].fillna(0) / (features[f'aps_in_active_days_{window}d'].fillna(0) + 1)) - \\\n",
    "                                                        (features[f'aps_out_{window}d_count'].fillna(0) / (features[f'aps_out_active_days_{window}d'].fillna(0) + 1))\n",
    "        \n",
    "        # 流入占比\n",
    "        total_amt = features[f'aps_in_{window}d_sum'].fillna(0) + features[f'aps_out_{window}d_sum'].fillna(0)\n",
    "        features[f'aps_in_amt_pct_{window}d'] = features[f'aps_in_{window}d_sum'].fillna(0) / (total_amt + 1e-5)\n",
    "        \n",
    "        # 流入笔数占比\n",
    "        total_count = features[f'aps_in_{window}d_count'].fillna(0) + features[f'aps_out_{window}d_count'].fillna(0)\n",
    "        features[f'aps_in_count_pct_{window}d'] = features[f'aps_in_{window}d_count'].fillna(0) / (total_count + 1e-5)\n",
    "        \n",
    "        # 笔均流入流出\n",
    "        features[f'aps_in_avg_per_txn_{window}d'] = features[f'aps_in_{window}d_sum'] / (features[f'aps_in_{window}d_count'] + 1)\n",
    "        features[f'aps_out_avg_per_txn_{window}d'] = features[f'aps_out_{window}d_sum'] / (features[f'aps_out_{window}d_count'] + 1)\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"流入流出特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3740b1a",
   "metadata": {},
   "source": [
    "### 3. 时序趋势特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "3fae6a17",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "时序趋势特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def time_trend_features(df):\n",
    "    \"\"\"\n",
    "    时序趋势特征:近期vs远期对比、环比变化等\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    # 月度分段统计\n",
    "    periods = [(0, 30, 'm1'), (30, 60, 'm2'), (60, 90, 'm3')]\n",
    "    \n",
    "    for start, end, period_name in tqdm(periods, desc='时序趋势特征-月度'):\n",
    "        period_df = df[(df['days_to_now'] >= start) & (df['days_to_now'] < end)]\n",
    "        \n",
    "        if len(period_df) > 0:\n",
    "            # 金额统计\n",
    "            period_amt = period_df.groupby('CUST_NO')['APSDTRAMT_abs'].agg(['sum', 'mean', 'count']).reset_index()\n",
    "            period_amt.columns = ['CUST_NO', f'aps_{period_name}_amt_sum', f'aps_{period_name}_amt_mean', f'aps_{period_name}_count']\n",
    "            features = features.merge(period_amt, on='CUST_NO', how='left')\n",
    "            \n",
    "            # 活跃天数\n",
    "            period_active = period_df.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "            period_active.columns = ['CUST_NO', f'aps_{period_name}_active_days']\n",
    "            features = features.merge(period_active, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 环比增长率\n",
    "    features['aps_m1_m2_amt_growth'] = (features['aps_m1_amt_sum'] - features['aps_m2_amt_sum']) / (features['aps_m2_amt_sum'] + 1e-5)\n",
    "    features['aps_m2_m3_amt_growth'] = (features['aps_m2_amt_sum'] - features['aps_m3_amt_sum']) / (features['aps_m3_amt_sum'] + 1e-5)\n",
    "    \n",
    "    features['aps_m1_m2_count_growth'] = (features['aps_m1_count'] - features['aps_m2_count']) / (features['aps_m2_count'] + 1e-5)\n",
    "    features['aps_m2_m3_count_growth'] = (features['aps_m2_count'] - features['aps_m3_count']) / (features['aps_m3_count'] + 1e-5)\n",
    "    \n",
    "    # 趋势判断\n",
    "    features['aps_amt_trend_increasing'] = ((features['aps_m1_amt_sum'].fillna(0) > features['aps_m2_amt_sum'].fillna(0)) & \n",
    "                                            (features['aps_m2_amt_sum'].fillna(0) > features['aps_m3_amt_sum'].fillna(0))).astype(int)\n",
    "    features['aps_amt_trend_decreasing'] = ((features['aps_m1_amt_sum'].fillna(0) < features['aps_m2_amt_sum'].fillna(0)) & \n",
    "                                            (features['aps_m2_amt_sum'].fillna(0) < features['aps_m3_amt_sum'].fillna(0))).astype(int)\n",
    "    \n",
    "    # 月度方差\n",
    "    month_amt_cols = ['aps_m1_amt_sum', 'aps_m2_amt_sum', 'aps_m3_amt_sum']\n",
    "    features['aps_monthly_amt_std'] = features[month_amt_cols].std(axis=1)\n",
    "    features['aps_monthly_amt_cv'] = features['aps_monthly_amt_std'] / (features[month_amt_cols].mean(axis=1) + 1e-5)\n",
    "    \n",
    "    # 周度统计波动\n",
    "    weekly_amt = df.groupby(['CUST_NO', 'week_of_year'])['APSDTRAMT_abs'].sum().reset_index()\n",
    "    weekly_std = weekly_amt.groupby('CUST_NO')['APSDTRAMT_abs'].agg(['std', 'mean']).reset_index()\n",
    "    weekly_std.columns = ['CUST_NO', 'aps_weekly_amt_std', 'aps_weekly_amt_mean']\n",
    "    weekly_std['aps_weekly_amt_cv'] = weekly_std['aps_weekly_amt_std'] / (weekly_std['aps_weekly_amt_mean'] + 1e-5)\n",
    "    features = features.merge(weekly_std, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 近期活跃度\n",
    "    recent_weeks = [(0, 1), (0, 2), (0, 4)]\n",
    "    for start_w, end_w in recent_weeks:\n",
    "        recent_df = df[(df['days_to_now'] >= start_w * 7) & (df['days_to_now'] < end_w * 7)]\n",
    "        if len(recent_df) > 0:\n",
    "            recent_active = recent_df.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "            recent_active.columns = ['CUST_NO', f'aps_active_days_w{start_w}_w{end_w}']\n",
    "            features = features.merge(recent_active, on='CUST_NO', how='left')\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"时序趋势特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ab18fc8",
   "metadata": {},
   "source": [
    "### 4. 周期性特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "fc27db35",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "周期性特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def periodicity_features(df):\n",
    "    \"\"\"\n",
    "    周期性特征:工作日/周末、月初/月中/月末、时段等\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    # 工作日vs周末\n",
    "    weekday_df = df[df['is_weekend'] == 0]\n",
    "    weekend_df = df[df['is_weekend'] == 1]\n",
    "    \n",
    "    for sub_df, prefix in tqdm([(weekday_df, 'weekday'), (weekend_df, 'weekend')], desc='周期性特征-工作日/周末'):\n",
    "        if len(sub_df) > 0:\n",
    "            stats = sub_df.groupby('CUST_NO').agg({\n",
    "                'APSDTRAMT_abs': ['sum', 'mean', 'count', 'max', 'std'],\n",
    "                'APSDTRDAT': 'nunique'\n",
    "            }).reset_index()\n",
    "            stats.columns = ['CUST_NO', f'aps_{prefix}_amt_sum', f'aps_{prefix}_amt_mean',\n",
    "                              f'aps_{prefix}_count', f'aps_{prefix}_amt_max', f'aps_{prefix}_amt_std',\n",
    "                              f'aps_{prefix}_active_days']\n",
    "            features = features.merge(stats, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 工作日/周末比率\n",
    "    features['aps_weekday_weekend_amt_ratio'] = features['aps_weekday_amt_sum'] / (features['aps_weekend_amt_sum'] + 1e-5)\n",
    "    features['aps_weekday_weekend_count_ratio'] = features['aps_weekday_count'] / (features['aps_weekend_count'] + 1e-5)\n",
    "    features['aps_weekday_amt_pct'] = features['aps_weekday_amt_sum'] / (features['aps_weekday_amt_sum'].fillna(0) + features['aps_weekend_amt_sum'].fillna(0) + 1e-5)\n",
    "    \n",
    "    # 月初/月中/月末\n",
    "    for period, cond in [('month_start', df['is_month_start'] == 1), \n",
    "                          ('month_middle', df['is_month_middle'] == 1),\n",
    "                          ('month_end', df['is_month_end'] == 1)]:\n",
    "        period_df = df[cond]\n",
    "        if len(period_df) > 0:\n",
    "            period_stats = period_df.groupby('CUST_NO').agg({\n",
    "                'APSDTRAMT_abs': ['sum', 'mean', 'count'],\n",
    "                'APSDTRDAT': 'nunique'\n",
    "            }).reset_index()\n",
    "            period_stats.columns = ['CUST_NO', f'aps_{period}_amt_sum', f'aps_{period}_amt_mean',\n",
    "                                     f'aps_{period}_count', f'aps_{period}_active_days']\n",
    "            features = features.merge(period_stats, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 时段特征\n",
    "    if 'time_period' in df.columns:\n",
    "        for period in ['dawn', 'morning', 'afternoon', 'night']:\n",
    "            period_df = df[df['time_period'] == period]\n",
    "            if len(period_df) > 0:\n",
    "                period_stats = period_df.groupby('CUST_NO').agg({\n",
    "                    'APSDTRAMT_abs': ['sum', 'mean', 'count']\n",
    "                }).reset_index()\n",
    "                period_stats.columns = ['CUST_NO', f'aps_{period}_amt_sum', f'aps_{period}_amt_mean', f'aps_{period}_count']\n",
    "                features = features.merge(period_stats, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 工作时间交易占比\n",
    "        work_hour_df = df[df['is_work_hour'] == 1]\n",
    "        if len(work_hour_df) > 0:\n",
    "            work_stats = work_hour_df.groupby('CUST_NO').agg({\n",
    "                'APSDTRAMT_abs': ['sum', 'count']\n",
    "            }).reset_index()\n",
    "            work_stats.columns = ['CUST_NO', 'aps_work_hour_amt_sum', 'aps_work_hour_count']\n",
    "            features = features.merge(work_stats, on='CUST_NO', how='left')\n",
    "            \n",
    "            # 总交易统计\n",
    "            total_stats = df.groupby('CUST_NO').agg({\n",
    "                'APSDTRAMT_abs': ['sum'],\n",
    "                'CUST_NO': 'count'\n",
    "            }).reset_index()\n",
    "            total_stats.columns = ['CUST_NO', 'aps_total_amt_sum', 'aps_total_count']\n",
    "            features = features.merge(total_stats, on='CUST_NO', how='left')\n",
    "            \n",
    "            features['aps_work_hour_amt_pct'] = features['aps_work_hour_amt_sum'] / (features['aps_total_amt_sum'] + 1e-5)\n",
    "            features['aps_work_hour_count_pct'] = features['aps_work_hour_count'] / (features['aps_total_count'] + 1e-5)\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"周期性特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef065756",
   "metadata": {},
   "source": [
    "### 5. 分类字段统计特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "2dd3c11d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "分类字段统计特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def categorical_features(df, windows=[30, 60, 90]):\n",
    "    \"\"\"\n",
    "    分类字段统计特征:交易码、渠道、摘要、三方标识等\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    # 分类字段列表\n",
    "    cat_cols = ['APSDTRCOD', 'APSDTRCHL', 'APSDABS', 'APSDFLAG']\n",
    "    \n",
    "    for window in tqdm(windows, desc='分类字段统计特征'):\n",
    "        sub_df = df[df['days_to_now'] < window].copy()\n",
    "        \n",
    "        if len(sub_df) == 0:\n",
    "            continue\n",
    "        \n",
    "        for col in cat_cols:\n",
    "            if col in sub_df.columns:\n",
    "                # 唯一值数量\n",
    "                nunique = sub_df.groupby('CUST_NO')[col].nunique().reset_index()\n",
    "                nunique.columns = ['CUST_NO', f'aps_{col}_nunique_{window}d']\n",
    "                features = features.merge(nunique, on='CUST_NO', how='left')\n",
    "                \n",
    "                # 最频繁值占比\n",
    "                def get_most_common_ratio(x):\n",
    "                    if len(x) == 0:\n",
    "                        return 0\n",
    "                    return x.value_counts().iloc[0] / len(x)\n",
    "                \n",
    "                most_common = sub_df.groupby('CUST_NO')[col].apply(get_most_common_ratio).reset_index()\n",
    "                most_common.columns = ['CUST_NO', f'aps_{col}_most_common_ratio_{window}d']\n",
    "                features = features.merge(most_common, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 三方交易统计\n",
    "    if 'is_third_party' in df.columns:\n",
    "        for window in windows:\n",
    "            third_df = df[(df['days_to_now'] < window) & (df['is_third_party'] == 1)]\n",
    "            \n",
    "            if len(third_df) > 0:\n",
    "                third_stats = third_df.groupby('CUST_NO').agg({\n",
    "                    'APSDTRAMT_abs': ['sum', 'mean', 'count']\n",
    "                }).reset_index()\n",
    "                third_stats.columns = ['CUST_NO', f'aps_third_amt_sum_{window}d', \n",
    "                                        f'aps_third_amt_mean_{window}d', f'aps_third_count_{window}d']\n",
    "                features = features.merge(third_stats, on='CUST_NO', how='left')\n",
    "                \n",
    "                # 三方交易占比\n",
    "                total = df[df['days_to_now'] < window].groupby('CUST_NO').size().reset_index(name='total')\n",
    "                features = features.merge(total, on='CUST_NO', how='left')\n",
    "                features[f'aps_third_count_pct_{window}d'] = features[f'aps_third_count_{window}d'] / (features['total'] + 1)\n",
    "                features.drop('total', axis=1, inplace=True)\n",
    "    \n",
    "    # Top N 交易码统计\n",
    "    if 'APSDTRCOD' in df.columns:\n",
    "        top_codes = df['APSDTRCOD'].value_counts().head(20).index.tolist()\n",
    "        \n",
    "        for window in windows:\n",
    "            sub_df = df[df['days_to_now'] < window]\n",
    "            \n",
    "            for i, code in enumerate(top_codes[:10], 1):\n",
    "                code_df = sub_df[sub_df['APSDTRCOD'] == code]\n",
    "                if len(code_df) > 0:\n",
    "                    code_count = code_df.groupby('CUST_NO').size().reset_index(name=f'aps_code_top{i}_count_{window}d')\n",
    "                    features = features.merge(code_count, on='CUST_NO', how='left')\n",
    "                    \n",
    "                    code_amt = code_df.groupby('CUST_NO')['APSDTRAMT_abs'].sum().reset_index()\n",
    "                    code_amt.columns = ['CUST_NO', f'aps_code_top{i}_amt_{window}d']\n",
    "                    features = features.merge(code_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    # Top N 渠道统计\n",
    "    if 'APSDTRCHL' in df.columns:\n",
    "        top_channels = df['APSDTRCHL'].value_counts().head(10).index.tolist()\n",
    "        \n",
    "        for window in windows:\n",
    "            sub_df = df[df['days_to_now'] < window]\n",
    "            \n",
    "            for i, channel in enumerate(top_channels[:5], 1):\n",
    "                channel_df = sub_df[sub_df['APSDTRCHL'] == channel]\n",
    "                if len(channel_df) > 0:\n",
    "                    channel_count = channel_df.groupby('CUST_NO').size().reset_index(name=f'aps_channel_top{i}_count_{window}d')\n",
    "                    features = features.merge(channel_count, on='CUST_NO', how='left')\n",
    "                    \n",
    "                    channel_amt = channel_df.groupby('CUST_NO')['APSDTRAMT_abs'].sum().reset_index()\n",
    "                    channel_amt.columns = ['CUST_NO', f'aps_channel_top{i}_amt_{window}d']\n",
    "                    features = features.merge(channel_amt, on='CUST_NO', how='left')\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"分类字段统计特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6cd069b5",
   "metadata": {},
   "source": [
    "### 6. RFM特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "4dfe9c4a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "RFM特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def rfm_features(df):\n",
    "    \"\"\"\n",
    "    RFM特征: Recency(最近一次), Frequency(频率), Monetary(金额)\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    # R - 最近一次交易距今天数\n",
    "    recency = df.groupby('CUST_NO')['days_to_now'].min().reset_index()\n",
    "    recency.columns = ['CUST_NO', 'aps_recency_days']\n",
    "    features = features.merge(recency, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 最近一次流入/流出距今天数\n",
    "    df_in = df[df['is_income'] == 1]\n",
    "    df_out = df[df['is_income'] == 0]\n",
    "    \n",
    "    if len(df_in) > 0:\n",
    "        recency_in = df_in.groupby('CUST_NO')['days_to_now'].min().reset_index()\n",
    "        recency_in.columns = ['CUST_NO', 'aps_recency_in_days']\n",
    "        features = features.merge(recency_in, on='CUST_NO', how='left')\n",
    "    \n",
    "    if len(df_out) > 0:\n",
    "        recency_out = df_out.groupby('CUST_NO')['days_to_now'].min().reset_index()\n",
    "        recency_out.columns = ['CUST_NO', 'aps_recency_out_days']\n",
    "        features = features.merge(recency_out, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 最近一次大额交易距今天数 (>10000)\n",
    "    large_txn = df[df['APSDTRAMT_abs'] > 10000]\n",
    "    if len(large_txn) > 0:\n",
    "        recency_large = large_txn.groupby('CUST_NO')['days_to_now'].min().reset_index()\n",
    "        recency_large.columns = ['CUST_NO', 'aps_recency_large_days']\n",
    "        features = features.merge(recency_large, on='CUST_NO', how='left')\n",
    "    \n",
    "    # F - 频率特征\n",
    "    frequency = df.groupby('CUST_NO').size().reset_index(name='aps_frequency_total')\n",
    "    features = features.merge(frequency, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 活跃天数\n",
    "    active_days_total = df.groupby('CUST_NO')['APSDTRDAT'].nunique().reset_index()\n",
    "    active_days_total.columns = ['CUST_NO', 'aps_active_days_total']\n",
    "    features = features.merge(active_days_total, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 日均频率\n",
    "    features['aps_daily_frequency'] = features['aps_frequency_total'] / (features['aps_active_days_total'] + 1)\n",
    "    \n",
    "    # M - 金额特征\n",
    "    monetary = df.groupby('CUST_NO')['APSDTRAMT_abs'].agg(['sum', 'mean', 'max']).reset_index()\n",
    "    monetary.columns = ['CUST_NO', 'aps_monetary_total', 'aps_monetary_mean', 'aps_monetary_max']\n",
    "    features = features.merge(monetary, on='CUST_NO', how='left')\n",
    "    \n",
    "    # RFM综合得分(归一化后综合)\n",
    "    for col in ['aps_recency_days', 'aps_frequency_total', 'aps_monetary_total']:\n",
    "        if col in features.columns:\n",
    "            if col == 'aps_recency_days':\n",
    "                features[f'{col}_score'] = 1 / (features[col] + 1)\n",
    "            else:\n",
    "                col_max = features[col].max()\n",
    "                if col_max > 0:\n",
    "                    features[f'{col}_score'] = features[col] / col_max\n",
    "    \n",
    "    if all(c in features.columns for c in ['aps_recency_days_score', 'aps_frequency_total_score', 'aps_monetary_total_score']):\n",
    "        features['aps_rfm_score'] = (features['aps_recency_days_score'] + \n",
    "                                      features['aps_frequency_total_score'] + \n",
    "                                      features['aps_monetary_total_score']) / 3\n",
    "    \n",
    "    # 首次交易距今天数\n",
    "    first_txn = df.groupby('CUST_NO')['days_to_now'].max().reset_index()\n",
    "    first_txn.columns = ['CUST_NO', 'aps_first_txn_days']\n",
    "    features = features.merge(first_txn, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 客户生命周期长度\n",
    "    features['aps_lifetime_days'] = features['aps_first_txn_days'] - features['aps_recency_days']\n",
    "    \n",
    "    # 生命周期活跃率\n",
    "    features['aps_lifetime_activity_rate'] = features['aps_active_days_total'] / (features['aps_lifetime_days'] + 1)\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"RFM特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13d6f5db",
   "metadata": {},
   "source": [
    "### 7. 序列特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "bdadd7ba",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "序列特征函数已定义\n"
     ]
    }
   ],
   "source": [
    "def sequence_features(df, top_n=10):\n",
    "    \"\"\"\n",
    "    序列特征:交易间隔、连续性等\n",
    "    \"\"\"\n",
    "    features = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    \n",
    "    # 按客户分组计算\n",
    "    for cust, group in tqdm(df.groupby('CUST_NO'), desc='序列特征', total=df['CUST_NO'].nunique()):\n",
    "        if len(group) < 2:\n",
    "            continue\n",
    "        \n",
    "        group_sorted = group.sort_values('APSDTRDAT').reset_index(drop=True)\n",
    "        \n",
    "        # 交易间隔天数\n",
    "        group_sorted['date_diff'] = group_sorted['APSDTRDAT'].diff().dt.days\n",
    "        \n",
    "        # 间隔统计\n",
    "        interval_stats = {\n",
    "            'CUST_NO': cust,\n",
    "            'aps_interval_mean': group_sorted['date_diff'].mean(),\n",
    "            'aps_interval_std': group_sorted['date_diff'].std(),\n",
    "            'aps_interval_max': group_sorted['date_diff'].max(),\n",
    "            'aps_interval_min': group_sorted['date_diff'].min()\n",
    "        }\n",
    "        \n",
    "        # 连续交易天数(间隔<=1天)\n",
    "        consecutive_days = (group_sorted['date_diff'] <= 1).sum()\n",
    "        interval_stats['aps_consecutive_days'] = consecutive_days\n",
    "        \n",
    "        # 添加到features\n",
    "        temp_df = pd.DataFrame([interval_stats])\n",
    "        features = features.merge(temp_df, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 只处理前top_n个客户以避免过长时间\n",
    "        if features.index.max() >= top_n * 1000:\n",
    "            break\n",
    "    \n",
    "    # 简化版:整体统计\n",
    "    if 'aps_interval_mean' not in features.columns or features['aps_interval_mean'].isna().sum() > 0.5 * len(features):\n",
    "        # 计算每个客户的日期唯一值和交易次数\n",
    "        date_stats = df.groupby('CUST_NO').agg({\n",
    "            'APSDTRDAT': ['min', 'max', 'nunique'],\n",
    "            'CUST_NO': 'count'\n",
    "        }).reset_index()\n",
    "        date_stats.columns = ['CUST_NO', 'first_date', 'last_date', 'unique_dates', 'total_txns']\n",
    "        \n",
    "        # 计算平均间隔(总天数/活跃天数)\n",
    "        date_stats['days_span'] = (date_stats['last_date'] - date_stats['first_date']).dt.days\n",
    "        date_stats['aps_avg_interval_approx'] = date_stats['days_span'] / (date_stats['unique_dates'] + 1)\n",
    "        date_stats['aps_txns_per_day'] = date_stats['total_txns'] / (date_stats['unique_dates'] + 1)\n",
    "        \n",
    "        features = features.merge(date_stats[['CUST_NO', 'aps_avg_interval_approx', 'aps_txns_per_day']], \n",
    "                                   on='CUST_NO', how='left')\n",
    "    \n",
    "    # 最近N笔交易金额特征\n",
    "    for n in [3, 5, 10]:\n",
    "        recent_n = df.groupby('CUST_NO').head(n).groupby('CUST_NO')['APSDTRAMT_abs'].agg(['mean', 'sum', 'std']).reset_index()\n",
    "        recent_n.columns = ['CUST_NO', f'aps_recent{n}_amt_mean', f'aps_recent{n}_amt_sum', f'aps_recent{n}_amt_std']\n",
    "        features = features.merge(recent_n, on='CUST_NO', how='left')\n",
    "    \n",
    "    return features\n",
    "\n",
    "print(\"序列特征函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfe3a7b7",
   "metadata": {},
   "source": [
    "### 8. 主特征生成函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "784ce4ed",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "主特征生成函数已定义\n"
     ]
    }
   ],
   "source": [
    "def generate_tr_aps_features(df, feature_name='train'):\n",
    "    \"\"\"\n",
    "    生成活期交易明细表的所有特征\n",
    "    \n",
    "    参数:\n",
    "    - df: 预处理后的数据\n",
    "    - feature_name: 特征名称标识('train' 或 'test')\n",
    "    \n",
    "    返回:\n",
    "    - features: 特征DataFrame\n",
    "    \"\"\"\n",
    "    print(f\"\\n{'='*80}\")\n",
    "    print(f\"开始生成活期交易明细表特征 - {feature_name}\")\n",
    "    print(f\"{'='*80}\\n\")\n",
    "    \n",
    "    # 初始化特征DataFrame\n",
    "    all_customers = pd.DataFrame({'CUST_NO': df['CUST_NO'].unique()})\n",
    "    print(f\"总客户数: {len(all_customers):,}\\n\")\n",
    "    \n",
    "    # 1. 基础统计特征\n",
    "    print(\"\\n[1/7] 生成基础统计特征...\")\n",
    "    feat_basic = basic_statistics_features(df)\n",
    "    all_customers = all_customers.merge(feat_basic, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_basic.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_basic\n",
    "    gc.collect()\n",
    "    \n",
    "    # 2. 流入流出特征\n",
    "    print(\"\\n[2/7] 生成流入流出特征...\")\n",
    "    feat_inout = inflow_outflow_features(df)\n",
    "    all_customers = all_customers.merge(feat_inout, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_inout.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_inout\n",
    "    gc.collect()\n",
    "    \n",
    "    # 3. 时序趋势特征\n",
    "    print(\"\\n[3/7] 生成时序趋势特征...\")\n",
    "    feat_trend = time_trend_features(df)\n",
    "    all_customers = all_customers.merge(feat_trend, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_trend.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_trend\n",
    "    gc.collect()\n",
    "    \n",
    "    # 4. 周期性特征\n",
    "    print(\"\\n[4/7] 生成周期性特征...\")\n",
    "    feat_period = periodicity_features(df)\n",
    "    all_customers = all_customers.merge(feat_period, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_period.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_period\n",
    "    gc.collect()\n",
    "    \n",
    "    # 5. 分类字段统计特征\n",
    "    print(\"\\n[5/7] 生成分类字段统计特征...\")\n",
    "    feat_cat = categorical_features(df)\n",
    "    all_customers = all_customers.merge(feat_cat, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_cat.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_cat\n",
    "    gc.collect()\n",
    "    \n",
    "    # 6. RFM特征\n",
    "    print(\"\\n[6/7] 生成RFM特征...\")\n",
    "    feat_rfm = rfm_features(df)\n",
    "    all_customers = all_customers.merge(feat_rfm, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_rfm.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_rfm\n",
    "    gc.collect()\n",
    "    \n",
    "    # 7. 序列特征\n",
    "    print(\"\\n[7/7] 生成序列特征...\")\n",
    "    feat_seq = sequence_features(df)\n",
    "    all_customers = all_customers.merge(feat_seq, on='CUST_NO', how='left')\n",
    "    print(f\"   特征数: {feat_seq.shape[1]-1}, 总特征数: {all_customers.shape[1]-1}\")\n",
    "    del feat_seq\n",
    "    gc.collect()\n",
    "    \n",
    "    print(f\"\\n{'='*80}\")\n",
    "    print(f\"特征生成完成!\")\n",
    "    print(f\"最终特征维度: {all_customers.shape}\")\n",
    "    print(f\"特征数量: {all_customers.shape[1]-1}\")\n",
    "    print(f\"客户数: {all_customers.shape[0]:,}\")\n",
    "    print(f\"缺失值统计:\")\n",
    "    missing_pct = (all_customers.isnull().sum() / len(all_customers) * 100).sort_values(ascending=False)\n",
    "    print(missing_pct[missing_pct > 0].head(10))\n",
    "    print(f\"{'='*80}\\n\")\n",
    "    \n",
    "    return all_customers\n",
    "\n",
    "print(\"主特征生成函数已定义\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "765c4211",
   "metadata": {},
   "source": [
    "## 特征生成执行"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0f7d843",
   "metadata": {},
   "source": [
    "### 训练集特征生成"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "b25fae90",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "================================================================================\n",
      "开始生成活期交易明细表特征 - train\n",
      "================================================================================\n",
      "\n",
      "总客户数: 67,214\n",
      "\n",
      "\n",
      "[1/7] 生成基础统计特征...\n",
      "总客户数: 67,214\n",
      "\n",
      "\n",
      "[1/7] 生成基础统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "基础统计特征: 100%|██████████| 5/5 [02:10<00:00, 26.17s/it]\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 109, 总特征数: 109\n",
      "\n",
      "[2/7] 生成流入流出特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "流入流出特征: 100%|██████████| 5/5 [00:23<00:00,  4.60s/it]\n",
      "流入流出特征: 100%|██████████| 5/5 [00:23<00:00,  4.60s/it]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 140, 总特征数: 249\n",
      "\n",
      "[3/7] 生成时序趋势特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "时序趋势特征-月度: 100%|██████████| 3/3 [00:01<00:00,  1.57it/s]\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 26, 总特征数: 275\n",
      "\n",
      "[4/7] 生成周期性特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "周期性特征-工作日/周末: 100%|██████████| 2/2 [00:00<00:00,  2.64it/s]\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 45, 总特征数: 320\n",
      "\n",
      "[5/7] 生成分类字段统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "分类字段统计特征: 100%|██████████| 3/3 [01:35<00:00, 31.91s/it]\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 126, 总特征数: 446\n",
      "\n",
      "[6/7] 生成RFM特征...\n",
      "   特征数: 16, 总特征数: 462\n",
      "\n",
      "[7/7] 生成序列特征...\n",
      "   特征数: 16, 总特征数: 462\n",
      "\n",
      "[7/7] 生成序列特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "序列特征:   0%|          | 0/67214 [00:00<?, ?it/s]\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   特征数: 16, 总特征数: 478\n",
      "\n",
      "================================================================================\n",
      "特征生成完成!\n",
      "最终特征维度: (67214, 479)\n",
      "特征数量: 478\n",
      "客户数: 67,214\n",
      "缺失值统计:\n",
      "aps_consecutive_days       99.998512\n",
      "aps_interval_mean          99.998512\n",
      "aps_interval_std           99.998512\n",
      "aps_interval_max           99.998512\n",
      "aps_interval_min           99.998512\n",
      "aps_code_top9_count_30d    96.406999\n",
      "aps_code_top9_amt_30d      96.406999\n",
      "aps_code_top9_amt_60d      95.822299\n",
      "aps_code_top9_count_60d    95.822299\n",
      "aps_code_top9_count_90d    95.355134\n",
      "dtype: float64\n",
      "================================================================================\n",
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>aps_amt_7d_sum</th>\n",
       "      <th>aps_amt_7d_mean</th>\n",
       "      <th>aps_amt_7d_std</th>\n",
       "      <th>aps_amt_7d_median</th>\n",
       "      <th>aps_amt_7d_max</th>\n",
       "      <th>aps_amt_7d_min</th>\n",
       "      <th>aps_amt_7d_q25</th>\n",
       "      <th>aps_amt_7d_q75</th>\n",
       "      <th>aps_amt_7d_skew</th>\n",
       "      <th>...</th>\n",
       "      <th>aps_txns_per_day</th>\n",
       "      <th>aps_recent3_amt_mean</th>\n",
       "      <th>aps_recent3_amt_sum</th>\n",
       "      <th>aps_recent3_amt_std</th>\n",
       "      <th>aps_recent5_amt_mean</th>\n",
       "      <th>aps_recent5_amt_sum</th>\n",
       "      <th>aps_recent5_amt_std</th>\n",
       "      <th>aps_recent10_amt_mean</th>\n",
       "      <th>aps_recent10_amt_sum</th>\n",
       "      <th>aps_recent10_amt_std</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00010b067dfc59a47efebe6e5e353b7a</td>\n",
       "      <td>64.72</td>\n",
       "      <td>5.883636</td>\n",
       "      <td>6.772273</td>\n",
       "      <td>1.85</td>\n",
       "      <td>19.58</td>\n",
       "      <td>1.64</td>\n",
       "      <td>1.780</td>\n",
       "      <td>6.245</td>\n",
       "      <td>1.627162</td>\n",
       "      <td>...</td>\n",
       "      <td>2.500000</td>\n",
       "      <td>5.690000</td>\n",
       "      <td>17.07</td>\n",
       "      <td>3.762393</td>\n",
       "      <td>8.3520</td>\n",
       "      <td>41.76</td>\n",
       "      <td>6.494422</td>\n",
       "      <td>8.011000</td>\n",
       "      <td>80.11</td>\n",
       "      <td>5.890387</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>00018758fc8ec9ad4dd86aa4cff36375</td>\n",
       "      <td>0.23</td>\n",
       "      <td>0.230000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.23</td>\n",
       "      <td>0.23</td>\n",
       "      <td>0.23</td>\n",
       "      <td>0.230</td>\n",
       "      <td>0.230</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>40.040000</td>\n",
       "      <td>120.12</td>\n",
       "      <td>21.546993</td>\n",
       "      <td>30.0875</td>\n",
       "      <td>120.35</td>\n",
       "      <td>26.565472</td>\n",
       "      <td>30.087500</td>\n",
       "      <td>120.35</td>\n",
       "      <td>26.565472</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>00028d695fe5305c05342ed6cf61dadb</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>1.473684</td>\n",
       "      <td>6.896667</td>\n",
       "      <td>20.69</td>\n",
       "      <td>7.638366</td>\n",
       "      <td>5.1700</td>\n",
       "      <td>25.85</td>\n",
       "      <td>5.898614</td>\n",
       "      <td>4.002000</td>\n",
       "      <td>40.02</td>\n",
       "      <td>4.129005</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0002e2f3c781272101eacdd03052cff9</td>\n",
       "      <td>68.67</td>\n",
       "      <td>2.543333</td>\n",
       "      <td>0.956227</td>\n",
       "      <td>2.44</td>\n",
       "      <td>4.59</td>\n",
       "      <td>1.08</td>\n",
       "      <td>1.815</td>\n",
       "      <td>2.855</td>\n",
       "      <td>0.752452</td>\n",
       "      <td>...</td>\n",
       "      <td>2.422535</td>\n",
       "      <td>2.990000</td>\n",
       "      <td>8.97</td>\n",
       "      <td>1.993765</td>\n",
       "      <td>3.4080</td>\n",
       "      <td>17.04</td>\n",
       "      <td>2.219408</td>\n",
       "      <td>4.060000</td>\n",
       "      <td>40.60</td>\n",
       "      <td>1.758680</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0003f0fa65df0990b08033e59a281726</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>12.523333</td>\n",
       "      <td>37.57</td>\n",
       "      <td>0.479201</td>\n",
       "      <td>13.3360</td>\n",
       "      <td>66.68</td>\n",
       "      <td>2.166617</td>\n",
       "      <td>12.261111</td>\n",
       "      <td>110.35</td>\n",
       "      <td>3.034940</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 479 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  aps_amt_7d_sum  aps_amt_7d_mean  \\\n",
       "0  00010b067dfc59a47efebe6e5e353b7a           64.72         5.883636   \n",
       "1  00018758fc8ec9ad4dd86aa4cff36375            0.23         0.230000   \n",
       "2  00028d695fe5305c05342ed6cf61dadb             NaN              NaN   \n",
       "3  0002e2f3c781272101eacdd03052cff9           68.67         2.543333   \n",
       "4  0003f0fa65df0990b08033e59a281726             NaN              NaN   \n",
       "\n",
       "   aps_amt_7d_std  aps_amt_7d_median  aps_amt_7d_max  aps_amt_7d_min  \\\n",
       "0        6.772273               1.85           19.58            1.64   \n",
       "1             NaN               0.23            0.23            0.23   \n",
       "2             NaN                NaN             NaN             NaN   \n",
       "3        0.956227               2.44            4.59            1.08   \n",
       "4             NaN                NaN             NaN             NaN   \n",
       "\n",
       "   aps_amt_7d_q25  aps_amt_7d_q75  aps_amt_7d_skew  ...  aps_txns_per_day  \\\n",
       "0           1.780           6.245         1.627162  ...          2.500000   \n",
       "1           0.230           0.230              NaN  ...          1.000000   \n",
       "2             NaN             NaN              NaN  ...          1.473684   \n",
       "3           1.815           2.855         0.752452  ...          2.422535   \n",
       "4             NaN             NaN              NaN  ...          1.000000   \n",
       "\n",
       "   aps_recent3_amt_mean  aps_recent3_amt_sum  aps_recent3_amt_std  \\\n",
       "0              5.690000                17.07             3.762393   \n",
       "1             40.040000               120.12            21.546993   \n",
       "2              6.896667                20.69             7.638366   \n",
       "3              2.990000                 8.97             1.993765   \n",
       "4             12.523333                37.57             0.479201   \n",
       "\n",
       "   aps_recent5_amt_mean  aps_recent5_amt_sum  aps_recent5_amt_std  \\\n",
       "0                8.3520                41.76             6.494422   \n",
       "1               30.0875               120.35            26.565472   \n",
       "2                5.1700                25.85             5.898614   \n",
       "3                3.4080                17.04             2.219408   \n",
       "4               13.3360                66.68             2.166617   \n",
       "\n",
       "   aps_recent10_amt_mean  aps_recent10_amt_sum  aps_recent10_amt_std  \n",
       "0               8.011000                 80.11              5.890387  \n",
       "1              30.087500                120.35             26.565472  \n",
       "2               4.002000                 40.02              4.129005  \n",
       "3               4.060000                 40.60              1.758680  \n",
       "4              12.261111                110.35              3.034940  \n",
       "\n",
       "[5 rows x 479 columns]"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_features = generate_tr_aps_features(train_tr_aps_processed, feature_name='train')\n",
    "train_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "134a0844",
   "metadata": {},
   "source": [
    "### 测试集特征生成"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7906700",
   "metadata": {},
   "outputs": [],
   "source": [
    "# print(\"\\n开始预处理测试集...\")\n",
    "# A_tr_aps_processed = preprocess_tr_aps_data(A_tr_aps_dtl_data)\n",
    "\n",
    "# test_features = generate_tr_aps_features(A_tr_aps_processed, feature_name='test')\n",
    "# test_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "529ea5ac",
   "metadata": {},
   "source": [
    "## 特征保存"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50e26c71",
   "metadata": {},
   "source": [
    "### 训练集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "18ce601c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "保存训练集特征...\n",
      "  - TRAIN_TR_APS_DTL_features.pkl 已保存, 维度: (67214, 479)\n",
      "  - TRAIN_TR_APS_DTL_features.pkl 已保存, 维度: (67214, 479)\n"
     ]
    }
   ],
   "source": [
    "# 创建feature目录\n",
    "feature_dir = './feature/Train'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "# 保存训练集特征\n",
    "print(\"\\n保存训练集特征...\")\n",
    "with open(os.path.join(feature_dir, 'TRAIN_TR_APS_DTL_features.pkl'), 'wb') as f:\n",
    "    pickle.dump(train_features, f)\n",
    "print(f\"  - TRAIN_TR_APS_DTL_features.pkl 已保存, 维度: {train_features.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "808687fa",
   "metadata": {},
   "source": [
    "### 测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e6553aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 创建feature目录\n",
    "#feature_dir = './feature/A'\n",
    "#if not os.path.exists(feature_dir):\n",
    "#    os.makedirs(feature_dir)\n",
    "#    print(f\"创建特征目录: {feature_dir}\"\n",
    "          \n",
    "# test_feature_path = os.path.join(feature_dir, 'test_tr_aps_features.pkl')\n",
    "# with open(test_feature_path, 'wb') as f:\n",
    "#     pickle.dump(test_features, f)\n",
    "# print(f\"测试集特征已保存到: {test_feature_path}\")\n",
    "# print(f\"特征维度: {test_features.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c103913b",
   "metadata": {},
   "source": [
    "## 特征质量检查"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "f707ee3f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "特征质量检查\n",
      "================================================================================\n",
      "\n",
      "1. 特征维度信息:\n",
      "   总特征数: 478\n",
      "   客户数: 67,214\n",
      "\n",
      "2. 缺失值统计:\n",
      "                            缺失数  缺失率(%)\n",
      "aps_consecutive_days      67213  100.00\n",
      "aps_interval_mean         67213  100.00\n",
      "aps_interval_std          67213  100.00\n",
      "aps_interval_max          67213  100.00\n",
      "aps_interval_min          67213  100.00\n",
      "aps_code_top9_count_30d   64799   96.41\n",
      "aps_code_top9_amt_30d     64799   96.41\n",
      "aps_code_top9_amt_60d     64406   95.82\n",
      "aps_code_top9_count_60d   64406   95.82\n",
      "aps_code_top9_count_90d   64092   95.36\n",
      "aps_code_top9_amt_90d     64092   95.36\n",
      "aps_code_top10_amt_30d    61512   91.52\n",
      "aps_code_top10_count_30d  61512   91.52\n",
      "aps_code_top10_count_60d  59508   88.54\n",
      "aps_code_top10_amt_60d    59508   88.54\n",
      "aps_code_top8_amt_30d     58088   86.42\n",
      "aps_code_top8_count_30d   58088   86.42\n",
      "aps_code_top10_amt_90d    58042   86.35\n",
      "aps_code_top10_count_90d  58042   86.35\n",
      "aps_in_7d_skew            55894   83.16\n",
      "\n",
      "3. 常量特征检查:\n",
      "   发现 5 个常量特征:\n",
      "   ['aps_interval_mean', 'aps_interval_std', 'aps_interval_max', 'aps_interval_min', 'aps_consecutive_days']\n",
      "\n",
      "4. 无穷值检查:\n",
      "   未发现无穷值\n",
      "\n",
      "5. 特征类型分布:\n",
      "float64    470\n",
      "int64        6\n",
      "int32        2\n",
      "object       1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "6. 特征统计摘要(前10个特征):\n",
      "   发现 5 个常量特征:\n",
      "   ['aps_interval_mean', 'aps_interval_std', 'aps_interval_max', 'aps_interval_min', 'aps_consecutive_days']\n",
      "\n",
      "4. 无穷值检查:\n",
      "   未发现无穷值\n",
      "\n",
      "5. 特征类型分布:\n",
      "float64    470\n",
      "int64        6\n",
      "int32        2\n",
      "object       1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "6. 特征统计摘要(前10个特征):\n",
      "                     count       mean         std       min        25%  \\\n",
      "aps_amt_7d_sum     43679.0  95.630166  216.590148  0.000000  12.640000   \n",
      "aps_amt_7d_mean    43679.0  10.512235   11.297326  0.000000   3.546264   \n",
      "aps_amt_7d_std     34641.0   6.291071    7.317786  0.000000   1.647559   \n",
      "aps_amt_7d_median  43679.0   9.731919   11.837282  0.000000   2.930000   \n",
      "aps_amt_7d_max     43679.0  19.090842   20.032989  0.000000   5.610000   \n",
      "aps_amt_7d_min     43679.0   5.468130    8.869742  0.000000   1.385000   \n",
      "aps_amt_7d_q25     43679.0   7.563848    9.898656  0.000000   2.300000   \n",
      "aps_amt_7d_q75     43679.0  12.689457   14.141093  0.000000   3.940000   \n",
      "aps_amt_7d_skew    29188.0   0.817956    1.294993 -3.809338   0.000000   \n",
      "aps_amt_7d_kurt    25567.0   1.708257    4.967326 -6.000000  -0.958245   \n",
      "\n",
      "                         50%         75%           max  \n",
      "aps_amt_7d_sum     39.790000  104.745000  17957.750000  \n",
      "aps_amt_7d_mean     6.683333   13.640000    211.445000  \n",
      "aps_amt_7d_std      4.046878    8.256174    114.100869  \n",
      "aps_amt_7d_median   5.100000   12.630000    232.680000  \n",
      "aps_amt_7d_max     13.610000   23.765000    312.970000  \n",
      "aps_amt_7d_min      2.310000    5.310000    211.400000  \n",
      "aps_amt_7d_q25      3.732500    9.021250    211.422500  \n",
      "aps_amt_7d_q75      7.685000   16.970000    232.680000  \n",
      "aps_amt_7d_skew     0.871257    1.704362     13.798601  \n",
      "aps_amt_7d_kurt     0.628416    3.358109    227.561408  \n",
      "\n",
      "================================================================================\n",
      "                     count       mean         std       min        25%  \\\n",
      "aps_amt_7d_sum     43679.0  95.630166  216.590148  0.000000  12.640000   \n",
      "aps_amt_7d_mean    43679.0  10.512235   11.297326  0.000000   3.546264   \n",
      "aps_amt_7d_std     34641.0   6.291071    7.317786  0.000000   1.647559   \n",
      "aps_amt_7d_median  43679.0   9.731919   11.837282  0.000000   2.930000   \n",
      "aps_amt_7d_max     43679.0  19.090842   20.032989  0.000000   5.610000   \n",
      "aps_amt_7d_min     43679.0   5.468130    8.869742  0.000000   1.385000   \n",
      "aps_amt_7d_q25     43679.0   7.563848    9.898656  0.000000   2.300000   \n",
      "aps_amt_7d_q75     43679.0  12.689457   14.141093  0.000000   3.940000   \n",
      "aps_amt_7d_skew    29188.0   0.817956    1.294993 -3.809338   0.000000   \n",
      "aps_amt_7d_kurt    25567.0   1.708257    4.967326 -6.000000  -0.958245   \n",
      "\n",
      "                         50%         75%           max  \n",
      "aps_amt_7d_sum     39.790000  104.745000  17957.750000  \n",
      "aps_amt_7d_mean     6.683333   13.640000    211.445000  \n",
      "aps_amt_7d_std      4.046878    8.256174    114.100869  \n",
      "aps_amt_7d_median   5.100000   12.630000    232.680000  \n",
      "aps_amt_7d_max     13.610000   23.765000    312.970000  \n",
      "aps_amt_7d_min      2.310000    5.310000    211.400000  \n",
      "aps_amt_7d_q25      3.732500    9.021250    211.422500  \n",
      "aps_amt_7d_q75      7.685000   16.970000    232.680000  \n",
      "aps_amt_7d_skew     0.871257    1.704362     13.798601  \n",
      "aps_amt_7d_kurt     0.628416    3.358109    227.561408  \n",
      "\n",
      "================================================================================\n"
     ]
    }
   ],
   "source": [
    "print(\"=\" * 80)\n",
    "print(\"特征质量检查\")\n",
    "print(\"=\" * 80)\n",
    "\n",
    "print(\"\\n1. 特征维度信息:\")\n",
    "print(f\"   总特征数: {train_features.shape[1] - 1}\")\n",
    "print(f\"   客户数: {train_features.shape[0]:,}\")\n",
    "\n",
    "print(\"\\n2. 缺失值统计:\")\n",
    "missing_stats = train_features.isnull().sum().sort_values(ascending=False)\n",
    "missing_pct = (missing_stats / len(train_features) * 100).round(2)\n",
    "missing_df = pd.DataFrame({\n",
    "    '缺失数': missing_stats,\n",
    "    '缺失率(%)': missing_pct\n",
    "})\n",
    "print(missing_df[missing_df['缺失数'] > 0].head(20))\n",
    "\n",
    "print(\"\\n3. 常量特征检查:\")\n",
    "nunique = train_features.nunique()\n",
    "const_features = nunique[nunique == 1].index.tolist()\n",
    "if len(const_features) > 0:\n",
    "    print(f\"   发现 {len(const_features)} 个常量特征:\")\n",
    "    print(f\"   {const_features}\")\n",
    "else:\n",
    "    print(\"   未发现常量特征\")\n",
    "\n",
    "print(\"\\n4. 无穷值检查:\")\n",
    "inf_cols = []\n",
    "for col in train_features.select_dtypes(include=[np.number]).columns:\n",
    "    if np.isinf(train_features[col]).any():\n",
    "        inf_cols.append(col)\n",
    "if len(inf_cols) > 0:\n",
    "    print(f\"   发现 {len(inf_cols)} 个包含无穷值的特征:\")\n",
    "    print(f\"   {inf_cols[:10]}\")\n",
    "else:\n",
    "    print(\"   未发现无穷值\")\n",
    "\n",
    "print(\"\\n5. 特征类型分布:\")\n",
    "print(train_features.dtypes.value_counts())\n",
    "\n",
    "print(\"\\n6. 特征统计摘要(前10个特征):\")\n",
    "numeric_cols = train_features.select_dtypes(include=[np.number]).columns[:10]\n",
    "print(train_features[numeric_cols].describe().T)\n",
    "\n",
    "print(\"\\n\" + \"=\" * 80)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
